const oauth2orize = require('@poziworld/oauth2orize');
const passport = require('passport');
const login = require('connect-ensure-login');
const db = require('../db');
const utils = require('../utils');

// 创建 OAuth 2.0 服务
const server = oauth2orize.createServer();

// 注册序列化和反序列化函数。
//
// 当客户端将用户重定向到用户授权端点时，将启动授权事务。
// 要完成事务，用户必须验证并批准授权请求。
// 因为这可能涉及多个HTTP请求/响应交换，所以事务存储在会话中。
//
// 应用程序必须提供序列化函数，这些函数决定如何将客户端对象序列化到会话中。
// 通常，这将是一个简单的问题：序列化客户端的ID，然后通过从数据库中按ID查找客户端的反序列化。
server.serializeClient((client, done) => done(null, client.id));

server.deserializeClient((id, done) => {
  db.clients.findById(id, (error, client) => {
    if (error) return done(error);
    return done(null, client);
  });
});

function issueTokens(userId, clientId, done) {
  db.users.findById(userId, (error, user) => {
    const accessToken = utils.getUid(256);
    const refreshToken = utils.getUid(256);
    db.accessTokens.save(accessToken, userId, clientId, (error) => {
      if (error) return done(error);
      db.refreshTokens.save(refreshToken, userId, clientId, (error) => {
        if (error) return done(error);
        // 添加自定义参数，例如用户名username
        const params = { username: user.name };
        return done(null, accessToken, refreshToken, params);
      });
    });
  });
}

// Register supported grant types.
// OAuth 2.0指定了一个框架，允许用户向客户端应用程序授予对其受保护资源的有限访问。
// 它通过这样一个过程来实现：用户授予访问权限，客户机将授予权限交换为访问令牌。
// 
// 授予授权码。
// 回调接受请求授权的“客户端”、“redirectUri”（在随后的交换中用作验证器）、授权访问的经过身份验证的“用户”以及它们的响应，
// 其中包含经应用程序解析的批准的范围、持续时间等。
// 应用程序发出一个绑定到这些值的代码，并将其交换为访问令牌。
server.grant(oauth2orize.grant.code((client, redirectUri, user, ares, done) => {
  const code = utils.getUid(16);
  db.authorizationCodes.save(code, client.id, redirectUri, user.id, user.username, (error) => {
    if (error) return done(error);
    return done(null, code);
  });
}));

// 授予隐式授权。
// 回调接收请求授权的“客户端”、授权访问的经过身份验证的“用户”以及它们的响应，其中包含应用程序解析的已批准的范围、持续时间等。
// 应用程序发出一个令牌，该令牌绑定到这些值。
server.grant(oauth2orize.grant.token((client, user, ares, done) => {
  issueTokens(user.id, client.clientId, done);
}));

// 交换访问令牌的授权代码。
// 回调接受“客户端”，它正在交换“代码”和来自授权请求的任何“redirectUri”以进行验证。
// 如果验证了这些值，应用程序将代表授权代码的用户发出访问令牌。
// 发出的访问令牌响应可以包括刷新令牌和自定义参数，方法是将它们添加到‘ done() ’调用中
server.exchange(oauth2orize.exchange.code((client, code, redirectUri, done) => {
  db.authorizationCodes.find(code, (error, authCode) => {
    if (error) return done(error);
    if (client.id !== authCode.clientId) return done(null, false);
    if (redirectUri !== authCode.redirectUri) return done(null, false);

    issueTokens(authCode.userId, client.clientId, done);
  });
}));

// 交换访问令牌的用户id和密码。
// 回调接受‘ client ’，它从授权请求中交换用户名和密码以进行验证。
// 如果验证了这些值，应用程序将代表授权代码的用户发出访问令牌。
server.exchange(oauth2orize.exchange.password((client, username, password, scope, done) => {
  // Validate the client
  db.clients.findByClientId(client.clientId, (error, localClient) => {
    if (error) return done(error);
    if (!localClient) return done(null, false);
    if (localClient.clientSecret !== client.clientSecret) return done(null, false);
    // Validate the user
    db.users.findByUsername(username, (error, user) => {
      if (error) return done(error);
      if (!user) return done(null, false);
      if (password !== user.password) return done(null, false);
      // Everything validated, return the token
      issueTokens(user.id, client.clientId, done);
    });
  });
}));

// 将客户端id和密码/秘密交换为访问令牌。
// 回调接受‘ client’，它从授权请求中交换客户端的id和密码/秘密以进行验证。
// 如果验证了这些值，应用程序将代表授权代码的客户端发出访问令牌。
server.exchange(oauth2orize.exchange.clientCredentials((client, scope, done) => {
  // Validate the client
  db.clients.findByClientId(client.clientId, (error, localClient) => {
    if (error) return done(error);
    if (!localClient) return done(null, false);
    if (localClient.clientSecret !== client.clientSecret) return done(null, false);
    // Everything validated, return the token
    // Pass in a null for user id since there is no user with this grant type
    issueTokens(null, client.clientId, done);
  });
}));

// 发布新的令牌并删除旧的令牌
server.exchange(oauth2orize.exchange.refreshToken((client, refreshToken, scope, done) => {
  db.refreshTokens.find(refreshToken, (error, token) => {
    if (error) return done(error);
    issueTokens(token.id, client.id, (err, accessToken, refreshToken) => {
      if (err) {
        done(err, null, null);
      }
      db.accessTokens.removeByUserIdAndClientId(token.userId, token.clientId, (err) => {
        if (err) {
          done(err, null, null);
        }
        db.refreshTokens.removeByUserIdAndClientId(token.userId, token.clientId, (err) => {
          if (err) {
            done(err, null, null);
          }
          done(null, accessToken, refreshToken);
        });
      });
    });
  });
}));

// 用户授权端点。
//
// ‘authorization’中间件接受‘validate’回调，该回调负责验证发出授权请求的客户端。
// 在这样做时，建议根据注册值检查‘redirectUri’，尽管安全需求可能因实现而异。
// 一旦验证，‘done’回调必须与‘client’实例一起调用，以及‘redirectUri’，用户在获得授权决定后将被重定向到‘redirectUri’。

// 这个中间件只是初始化一个新的授权事务。
// 应用程序负责对用户进行身份验证并呈现对话框以获得他们的批准（显示有关请求授权的客户机的详细信息）。
// 我们在这里通过首先路由‘ensureLoggedIn()’，并呈现‘dialog’视图来实现。
module.exports.authorization = [
  login.ensureLoggedIn(),
  server.authorization((clientId, redirectUri, done) => {
    db.clients.findByClientId(clientId, (error, client) => {
      if (error) return done(error);
      // 警告：出于安全考虑，非常建议检查客户端提供的redirectUri是否与服务器注册的redirectUri匹配。
      // 为简单起见，本例没有这样做。
      return done(null, client, redirectUri);
    });
  }, (client, user, done) => {
    // 检查授权请求是否符合立即批准的条件
    // 自动批准
    if (client.isTrusted) return done(null, true);

    db.accessTokens.findByUserIdAndClientId(user.id, client.clientId, (error, token) => {
      // 自动批准
      if (token) return done(null, true);

      // 否则询问用户
      return done(null, false);
    });
  }),
  (request, response) => {
    response.render('dialog', { transactionId: request.oauth2.transactionID, user: request.user, client: request.oauth2.client });
  },
];

// 用户决策端点。
//
// ‘decision’中间件处理用户决定允许或拒绝客户端应用程序请求的访问。
// 根据客户端请求的授权类型，将调用上面配置的授权中间件来发送响应。
module.exports.decision = [
  login.ensureLoggedIn(),
  server.decision(),
];


// token端点。
//
// ‘token’中间件处理客户端请求以交换访问令牌的授权授予。
// 根据所交换的授权类型，将调用上述交换中间件来处理请求。
// 客户端在向该端点发出请求时必须进行身份验证。
module.exports.token = [
  passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
  server.token(),
  server.errorHandler(),
];
